查看原文
其他

利用 AdaNet 将多个 TensorFlow Hub 模块组合成一个集成网络

Google TensorFlow 2019-02-14

文 / Sara Robinson


您是否曾有过这样的经历:开始构建 ML 模型后,却发现不确定哪种模型架构会产生最佳效果?不妨试试基于 TensorFlow 的 AdaNet 框架吧。借助 AdaNet,您可以将多个模型馈送到 AdaNet 算法。然后,该算法会在训练过程中寻找所有这些模型的最优组合。我最近一直在尝试这一做法,相较于单个模型,这种集成学习的准确性尤其令我印象深刻。


稍等。在进一步探讨前,我们需要搞清楚:AdaNet 如何适应日益扩大的 ML 空间?这个问题其实是之前那篇 AdaNet 论文的开源实现。该篇论文简要介绍了一个名为 “神经架构搜索” 的概念,其中涉及将特定任务的最优 ML 模型架构设计流程自动化。由此可见,AdaNet 具备有理论支持的性能保证,而且运行速度极快。


这与 AutoML 有何关联?AutoML 涉及数据预处理、特征工程、模型族搜索和超参数调优,但不仅限于此。您可以将 AutoML 视作一个总括性概念,而 AdaNet 是 AutoML 模型架构搜索方面的一个框架。还需要注意的是,AutoML 研究 不同于 Google Cloud AutoML,后者是在底层利用 AutoML 概念,面向想要在不编写模型代码的情况下构建自定义 ML 模型的开发者(我写过很多有关 Cloud AutoML 的博文。)。


在本文中,我会向您介绍使用 AdaNet 的 AutoEnsembleEstimator 构建集成网络的全过程。您可以使用 AdaNet 构建任何类型的网络(图像、文本、结构化数据等)。例如,我要构建一个文本分类模型,用于根据给出的若干句子文本来预测这些文本的作者。在构建此模型时,除 AdaNet 之外,我还会用到下图所示工具:



另外,我还将展示如何使用 Cloud ML Engine 来大规模训练此模型。为了纪念 20 年间首次有新作品进入公共领域,我选择了一些在 1923 年有著作出版的作者,并将其用作训练数据。关于此示例的模型代码,请在 GitHub 中查看(https://github.com/sararob/adanet-ml-engine)


此示例使用 AdaNet 0.5.0、TensorFlow 1.12.0 和 TF Hub 0.2.0。


下面是我们将要为模型导入的软件包:

import adanet
import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow_hub as hub
import urllib

from sklearn.preprocessing import LabelEncoder



下载数据

我从古登堡工程下载了文献数据,并做了一些预处理,将每个文本拆分成多个包含 1-2 个句子的句段,并标记出相应的作者。预览如下:

text,author
I don't suppose the work   Much matters. You may work for all of me. I've seen the time I've had to work myself.,frost
Good. Beyond good and evil ? We are all that nowadays.,huxley
The peculiar dumb expression on her face was lost on Eliphalet. The overseer had laughed coarsely. What skeered on 'em? said he.,churchill


通过输入以下代码,我们可以使用 urllib 下载 CSV 文件,再将其转换为 Pandas DataFrame 文件,然后打乱数据,并进行预览:

urllib.request.urlretrieve('https://storage.googleapis.com/authors-training-data/data.csv', 'data.csv')

data = pd.read_csv('data.csv')
data = data.sample(frac=1) # Shuffles the data
data.head()


接下来,我们将其拆分为训练集和测试集,使用 80% 的数据进行训练:

train_size = int(len(data) * .8)

train_text = data['text'][:train_size]
train_authors = data['author'][:train_size]

test_text = data['text'][train_size:]
test_authors = data['author'][train_size:]


数据标签为每位作者的字符串,我使用 Scikit Learn LabelEncoder 实用程序将其编码为独热矢量。只需几行代码即可完成此项操作:

encoder = LabelEncoder()
encoder.fit_transform(np.array(train_authors))
train_encoded = encoder.transform(train_authors)
test_encoded = encoder.transform(test_authors)
num_classes = len(encoder.classes_)



使用 TF Hub 嵌入列

关于 TF Hub 文本模块,我最喜欢的一点是,您只用一行代码就可以实例化嵌套特征列,无需对文本输入做任何预处理,以使其转换为嵌入列。TF Hub 会处理这项繁重工作,因此,您可以不做任何预处理,直接将原始文本馈送进您的模型。关于 TF Hub,我曾在一篇博文中做过详细介绍,此处不再赘述。简而言之, 从零开始构建词嵌入需要大量时间和训练数据,而 TF Hub 提供有丰富的模型选择,我们可以从这里开始,让这一过程变得更简单。


我们可以从许多不同的 TF Hub 文本嵌入模块开始。(TF Hub 还提供有图像和视频模块。)应该选择哪一个?使用哪个模块处理我们的文本才能达到最高准确度,做出判断并不容易,而这正是 AdaNet 的用武之地。我们可以使用不同的 TF Hub 模块构建多个 TF 估算器,然后将其全部馈送至同一个 AdaNet 模型中,并让 AdaNet 进行集成,以寻找最优模型。


首先,我们来定义 TF Hub 嵌入列。在设置模型时,我们将通过这些嵌入列告诉 TensorFlow 应该针对我们的特征预期何种数据格式:

ndim_embeddings = hub.text_embedding_column(
 "ndim",
 module_spec="https://tfhub.dev/google/nnlm-en-dim128/1", trainable=False
)
encoder_embeddings = hub.text_embedding_column(
 "encoder",
 module_spec="https://tfhub.dev/google/universal-sentence-encoder/2", trainable=False)


现在,我们可以定义要馈送至 AdaNet 模型的两个估算器了。鉴于这是一个分类问题,我们会全部使用 DNNEstimator 来定义:

estimator_ndim = tf.contrib.estimator.DNNEstimator(
 head=multi_class_head,
 hidden_units=[64,10],
 feature_columns=[ndim_embeddings]
)

estimator_encoder = tf.contrib.estimator.DNNEstimator(
 head=multi_class_head,
 hidden_units=[64,10],
 feature_columns=[encoder_embeddings]
)


这里发生了什么?hidden_units 告诉 TensorFlow 我们的网络在每一层拥有多少个神经元。上图中的数据表示我们的网络在第一层有 64 个神经元,第二层有 10 个。feature_columns 是模型特征列表。在此示例中,我们只有一个特征(书中的句子)。



构建 AdaNet 模型

现在我们有两个估算器,并且可以随时将它们馈送至 AdaNet 模型中。在此示例中,我会使用 AdaNet 的 AutoEnsembleEstimator,这可以极大地简化馈送过程。它会选用我创建的两个估算器,然后通过计算每个模型的预测平均值来逐渐新建一个集成模型。如需更多自定义设置,请查看 adanet.subnetwork Builder 和 Generator 类。借助 AutoEnsembleEstimator,我们可以用 candidate_pool 参数将上面定义的两个模型馈送至集成模型中:

model_dir=os.path.join('/path/to/model/dir')

multi_class_head = tf.contrib.estimator.multi_class_head(
 len(encoder.classes_),
 loss_reduction=tf.losses.Reduction.SUM_OVER_BATCH_SIZE
)

estimator = adanet.AutoEnsembleEstimator(
   head=multi_class_head,
   candidate_pool=[
       estimator_ndim,
       estimator_encoder
   ],
   config=tf.estimator.RunConfig(
     save_summary_steps=1000,
     save_checkpoints_steps=1000,
     model_dir=model_dir
   ),
   max_iteration_steps=5000
)


这一过程要完成大量的工作,我们一条条来看:


  • head 是 tf.contrib.estimator.Head 的一个实例,它会告诉模型如何为每个可能的集成计算损失和评估指标。AdaNet 将这些潜在的集成网络称为 “候选网络”。head 的类型有很多,例如回归、多类别分类等。这里我们使用 multi_class_head,因为模型中有 2 个以上可能的标签类。如果是为一个特定输入指定多个标签的模型,我们会使用 multi_label_head。


  • config 会针对训练作业的运行设置一些参数:TF 保存模型摘要和检查点的频次以及存储目录。请记住,如果您在 Colab 中训练模型,过于频繁地保存检查点可能会消耗您的可用磁盘空间。


  • max_iteration_steps 告诉 AdaNet 单次 迭代需要执行多少训练步骤。迭代指的是对一组候选网络进行的训练,因此这个步数(和我们后面会定义的总训练步数)会告诉 AdaNet 生成新的候选集成网络的频率。


针对此过程,我们会使用 TensorFlow 的 train_and_evaluate 函数,该函数会同时执行训练和评估,非常方便。要完成此项设置,我们需要编写训练和评估的输入函数。输入函数负责将数据馈送到模型中。我们会在输入函数中使用 tf.data API。虽然我们的两个模型彼此独立,而且有不同的特征列,但我们可以将它们的特征放入同一个字典,因此我们只需要编写一个输入函数:

train_features = {
 "ndim": train_text,
 "encoder": train_text
}

def input_fn_train():
 dataset = tf.data.Dataset.from_tensor_slices((train_features, train_authors))
 dataset = dataset.repeat().shuffle(100).batch(64)
 iterator = dataset.make_one_shot_iterator()
 data, labels = iterator.get_next()
 return data, labels


我们的评估特征和输入函数看起来非常相似:

eval_features = {
 "ndim": test_text,
 "encoder": test_reviews
}

def input_fn_eval():
 dataset = tf.data.Dataset.from_tensor_slices((eval_features, test_authors))
 dataset = dataset.batch(64)
 iterator = dataset.make_one_shot_iterator()
 data, labels = iterator.get_next()
 return data, labels


我们就要大功告成了!训练之前,我们还要完成最后一步,即创建或者训练和评估规范。您可以将这一步视作把所有东西都糅合到一起。由于我们要一次性执行训练和评估,这些规范会告诉估算器在进行每项作业时运行哪个输入函数。

train_spec = tf.estimator.TrainSpec(
 input_fn=input_fn_train,
 max_steps=40000
)

eval_spec=tf.estimator.EvalSpec(
 input_fn=input_fn_eval,
 steps=None,
 start_delay_secs=10,
 throttle_secs=10
)


还记得我们在前面定义的 max_iteration_steps 吗?TrainSpec 中的 max_steps 参数表示完成训练需要执行的总步数。这意味着我们总共会进行 8 次迭代,也就是说有 8 组候选集成网络。


现在可以执行训练和评估了:

tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)



在 Cloud ML Engine 上训练 AdaNet 模型

如果您尝试在 Colab 中运行上述单元,则可能会达到内存限制。这就需要云来大展身手了。我们会使用 Cloud ML Engine 训练模型。为此,您需要创建一个 Google 云端平台 (GCP) 项目,并启用计费功能。要准备好在云端使用上文定义的模型,我们只需在本地将应用程序打包,格式如下:

setup.py
config.yaml

trainer/
 model.py
 __init__.py


您可以按自己的喜好为 trainer 目录命名,这是我们要连同模型一起上传到 ML Engine 的 Python 软件包。__init__.py 是一个空文件,model.py 则包含上述所有代码。setup.py 包含软件包的名称和版本信息,以及我们用来创建模型的任何 Python 软件包依赖项。


您可以在 config.yaml 中为训练指定任何特定于 Cloud 的参数。这些参数表示的内容类似于您是否会利用 GPU 或 TPU 进行训练,以及您的训练作业需要多少工作线程。您可以在 此处 找到所有配置选项(https://cloud.google.com/ml-engine/docs/tensorflow/machine-types)



导出模型以便执行传送

在启动训练作业前,我想提醒一下,您可以在前文提到的 py file 中添加一些代码,以便在完成训练后导出模型。如果您现在不关心这一点,请跳过并查看下一节。


我们将使用 LatestExporter 类导出模型。要创建导出器,我们需要定义一个传送输入函数。一开始这让我很困惑,但其实它和我们定义的其他输入函数并没有太大区别。该函数应该返回两项内容:在传送模型时,我们的模型应该预期的输入格式,以及服务器应该预期的输入格式。在我们的模型中,这两种格式是相同的,但在某些情况下,您可能希望先对输入数据做些预处理,然后再将其馈送至模型中。在我们的模型中,由于这两种格式相同,所以传送输入函数非常简单:

def serving_input_fn():
   feature_placeholders = {
     'ndim' : tf.placeholder(tf.string, [None]),
     'encoder' : tf.placeholder(tf.string, [None])
   }

   return tf.estimator.export.ServingInputReceiver(feature_placeholders, feature_placeholders)


如果我们之前使用的 TF Hub 模块不允许直接传递原始文本,而是要求必须将文本转换为整数,则我们的输入函数将在这一步返回两个不同的对象。


该函数定义好后,即可定义导出器:

exporter = tf.estimator.LatestExporter('exporter', serving_input_fn, exports_to_keep=None)


要调用 export(),我们还需要模型的最后一个检查点和来自该检查点的评估结果。我们可以通过以下代码获取这些项目:

latest_ckpt = tf.train.latest_checkpoint(model_dir)

last_eval = estimator.evaluate(
   input_fn_eval,
   checkpoint_path=latest_ckpt
)

exporter.export(estimator, model_dir, latest_ckpt, last_eval, is_the_final_export=True)


哇哦!当此导出器在 ML Engine 中运行时,它会保存最终模型。



使用 gcloud 开始训练作业

要在云中训练模型,我们需要创建 Cloud Storage 存储分区。这里也是模型检查点的存储位置。我们还会将 Tensorboard 指向此存储分区,以便在训练时查看模型指标。我最喜欢通过 gcloud CLI 启动 ML Engine 训练作业。首先,为此作业定义一些环境变量:

export JOB_ID=unique_job_name
export JOB_DIR=gs://your/gcs/bucket/path
export PACKAGE_PATH=trainer/
export MODULE=trainer.model
export REGION=your_cloud_project_region


将上图中的字符串替换为您项目的特定变量。然后,即可使用如下 gcloud 命令开始训练:

gcloud ml-engine jobs submit training $JOB_ID --package-path $PACKAGE_PATH --module-name $MODULE --job-dir $JOB_DIR --region $REGION --runtime-version 1.12 --python-version 3.5 --config config.yaml


如果命令执行正确,您应该会在控制台中看到一条消息,提示您的作业已加入队列。您可以从命令行流式传输日志,也可以导航至 Cloud 控制台上 ML Engine 中的作业选项卡:




在 TensorBoard 中将 AdaNet 训练可视化

您的训练作业正在运行,现在该做什么?很幸运,您不需要等待作业完成后再评估结果。您可以利用 TensorBoard,在训练执行期间,该工具能够使用在训练作业中创建的检查点文件,直观呈现准确度、损失以及其他指标。如果已经在本地安装了 TensorFlow,您会听到一个好消息 — 您有权通过命令行访问 TensorBoard 。


运行以下命令,将 TensorBoard 指向 Cloud Storage 上的日志目录:

tensorboard --logdir gs://your/gcs/checkpoint/path


然后,将您的浏览器指向 localhost:6006 以查看训练进度,并导航至标量选项卡:



坦白讲,直到现在,我一直避免使用 TensorBoard(这么多图表会令人望而却步!)但很快您就会看到,TensorBoard 可以让您更容易理解模型的运行状态,对 AdaNet 尤其有用。这里,我们只关注准确度和 adanet_loss 图表。先从准确度开始,请看 adanet_weighted_ensemble 图表:



请记住,我们的模型每次迭代要完成 5000 个步骤,这意味着每完成 5000 个步骤,AdaNet 就会生成新的候选集成网络(第一次迭代除外,因为第一次仅包含单个网络。)如果将鼠标悬停在图表上,您可以看到每条线代表的迭代和集成网络:



我们可以看到,在训练中的这一点上,在第 7 次迭代中生成的第二个集成网络 (t6_DNNEstimator1/eval) 的准确度最高。TensorBoard 真正向我们展现了使用 AdaNet 组合模型的力量,随着训练的继续,集成网络的准确度会提升,并且远高于单个网络自身的准确度(上图中左侧的粉色和淡蓝色线)。


损失(或错误)图中揭示的趋势则很相似:随着 AdaNet 继续生成和训练新的集成网络,错误量稳步下降。




使用导出的模型

如果您有遵循上文所述关于创建输入传送函数并导出模型的步骤,则完成训练后,您应该会在指定的 GCS 存储分区中看到该模型。在底层,AdaNet 会针对给定迭代导出损失(错误)最少的候选网络。在导出文件夹中,您会看到下列文件:



如果想在 ML Engine 上传送您的模型(我会在后续的博文中详述这一点),可以按照 此部署步骤,将 ML Engine 指向此存储分区(https://cloud.google.com/ml-engine/docs/tensorflow/deploying-models)。您也可以在本地下载这些文件,然后以您喜欢的方式传送模型。


如果只是卖关子而不用训练好的模型做 任何 预测,一定会让读者失望。下面我们就利用 ML Engine 的局部预测功能,从命令行使用训练好的模型做局部预测。我们只需要遵照与传送输入函数相同的格式,使用想要预测的输入创建一个按新行分隔的 JSON 文件即可。示例如下:

{"encoder": "A strange land indeed! Could it be one with his native New England? Did Congress assemble from the Antipodes?", "ndim": "A strange land indeed! Could it be one with his native New England? Did Congress assemble from the Antipodes?"}


然后,我们可以运行以下命令:

gcloud ml-engine local predict --model-dir=gs://path/to/saved_model.pb --json-instances=path/to/test.json


响应如下:

CLASS_IDS  CLASSES                                                                                               PROBABILITIES
[1]        [u'1']    [0.0043347785249352455, 0.8382837176322937, 0.12185576558113098, 0.025106186047196388, 0.010419543832540512]


这意味着模型预测此文本有 83% 的可能性是由标签数组中第一个索引对应的作者所写(我们可以通过记录上面的 encoder.classes_ 得到这一信息),这位作者是 Churchill。预测正确!



未来计划

现在您知道了如何使用 AdaNet 的 AutoEnsembleEstimator 来构建模型,以及如何在 Cloud ML Engine 上训练该模型。想要详细了解我在本篇博文中提到的内容吗?请查看下列资源:

  • 本文的完整演示代码

    (https://github.com/sararob/adanet-ml-engine)

  • 收到关于 AdaNet 的反馈?欢迎为 GitHub 做出贡献

  • 我的同事 Amy 有一篇很不错的博文专门介绍 tf.train_and_evaluate

    (http://amygdala.github.io/ml/tensorflow/cloud_ml_engine/2018/01/26/tf.html)

  • 阅读关于 AdaNet 的研究论文

    (http://proceedings.mlr.press/v70/cortes17a.html)

  • 阅读这篇博文,详细了解 TF Hub

    (http://proceedings.mlr.press/v70/cortes17a.html)

  • 详细了解 ML Engine(ml engine)

  • 查看其他 TF Hub 模块(https://tfhub.dev/)


还有一个问题:您是 Keras 的粉丝吗?AdaNet 团队目前正在研究新增对 Keras 的支持!您可以 追踪进展(https://github.com/tensorflow/adanet/issues/1)



更多 AI 相关阅读:




    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存